Use CodeVersioning for EnC#130159
Conversation
|
Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag |
There was a problem hiding this comment.
Pull request overview
This PR extends the existing code-versioning infrastructure so Edit-and-Continue (EnC) edits are represented as explicit IL code versions (distinct from ReJIT), and updates DAC/cDAC and debugger plumbing to treat only true ReJIT versions as ReJIT.
Changes:
- Adds a “source” discriminator for IL code versions (ReJIT vs EnC) plus an EnC version counter, and threads this through CoreCLR, data descriptors, and cDAC contracts.
- Updates ReJIT enumeration / DAC-facing ReJIT APIs to exclude EnC code versions (and excludes the synthetic default version from ReJIT ID enumeration).
- Adjusts IL retrieval paths in legacy cDAC/DAC DBI paths to read IL from the active EnC code version where appropriate; updates/extends unit tests and contract docs.
Show a summary per file
| File | Description |
|---|---|
| src/native/managed/cdac/tests/UnitTests/ReJITTests.cs | Updates expected ReJIT ID enumeration behavior and mocks ICodeVersions.IsReJIT. |
| src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.CodeVersions.cs | Extends mock IL code version node layout with Source/EnCVersion; updates builder helpers. |
| src/native/managed/cdac/tests/UnitTests/CodeVersionsTests.cs | Adds coverage asserting EnC/default versions are excluded from ReJIT detection and enumeration. |
| src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs | Filters ReJIT-related legacy SOS DAC behaviors using ICodeVersions.IsReJIT. |
| src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs | Fetches IL from active EnC code version when available; filters IL code version node APIs to ReJIT only. |
| src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataFrame.cs | Uses active EnC IL for local signature parsing when applicable. |
| src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ILCodeVersionNode.cs | Adds Source and EnCVersion fields to the ILCodeVersionNode contract data type. |
| src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CodeVersions_1.cs | Implements ICodeVersions.IsReJIT based on the new ILCodeVersionNode.Source. |
| src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICodeVersions.cs | Introduces public CodeVersionSource and new ICodeVersions.IsReJIT API. |
| src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/Extensions/IReJITExtensions.cs | Filters ReJIT ID enumeration to true ReJIT versions via cv.IsReJIT. |
| src/coreclr/vm/rejit.cpp | Tags configured ReJIT IL code versions with CodeVersionSource::kReJIT; filters ReJIT ID queries. |
| src/coreclr/vm/method.hpp | Makes EnC IL methods versionable via CodeVersionManager; enforces EnC/ReJIT mutual exclusion. |
| src/coreclr/vm/method.cpp | Returns active EnC IL from CodeVersionManager in MethodDesc::GetILHeader. |
| src/coreclr/vm/encee.cpp | Creates/activates EnC IL code versions via CodeVersionManager; records source and EnC version. |
| src/coreclr/vm/datadescriptor/datadescriptor.inc | Adds ILCodeVersionNode.Source and ILCodeVersionNode.EnCVersion to the CDAC descriptor. |
| src/coreclr/vm/codeversion.h | Adds CodeVersionSource enum and stores Source/EnCVersion on ILCodeVersionNode. |
| src/coreclr/vm/codeversion.cpp | Implements ILCodeVersion::IsReJIT / GetEnCVersion, plus node accessors/mutators. |
| src/coreclr/debug/ee/functioninfo.cpp | Treats only ReJIT (not EnC) explicit versions as “rejitted” for boundary mapping behavior. |
| src/coreclr/debug/ee/debugger.cpp | Updates debugger ReJIT-related checks to ignore EnC versions; tags created versions as ReJIT. |
| src/coreclr/debug/daccess/request.cpp | Filters DAC ReJIT data paths to only ReJIT IL code versions. |
| src/coreclr/debug/daccess/dacdbiimpl.cpp | Uses CodeVersionManager to resolve EnC versions and EnC IL in DBI helper paths. |
| docs/design/datacontracts/CodeVersions.md | Documents the new CodeVersionSource/IsReJIT concepts (but needs an EnCVersion row added). |
Copilot's findings
- Files reviewed: 22/22 changed files
- Comments generated: 4
| public MockILCodeVersionNode AddILCodeVersionNode(ulong versionId, uint rejitFlags, bool deoptimized = false, uint source = 1 /* CodeVersionSource.ReJIT */, ulong encVersion = 0) | ||
| { |
There was a problem hiding this comment.
Copilot's findings
Comments suppressed due to low confidence (1)
src/coreclr/debug/daccess/request.cpp:4693
- GetPendingReJITID now rejects the active ILCodeVersion unless ilVersion.IsReJIT() is true. However newly-requested ReJIT versions start in kStateRequested with CodeVersionSource still kUnknown (it isn't set until later in ConfigureILCodeVersion), so pending ReJIT queries will incorrectly return E_INVALIDARG. Reorder the checks to treat kStateRequested as a ReJIT-pending version even before Source is populated, and only use IsReJIT() to reject non-requested non-ReJIT (e.g., EnC) versions.
CodeVersionManager::LockHolder codeVersioningLockHolder;
ILCodeVersion ilVersion = pCodeVersionManager->GetActiveILCodeVersion(pMD);
if (ilVersion.IsNull() || !ilVersion.IsReJIT())
{
hr = E_INVALIDARG;
}
else if (ilVersion.GetRejitState() == RejitFlags::kStateRequested)
{
- Files reviewed: 24/24 changed files
- Comments generated: 3
| public enum CodeVersionSource : uint | ||
| { | ||
| Unknown = 0, | ||
| ReJIT = 1, | ||
| EnC = 2, | ||
| } |
| _codeVersionsAllocator.Allocate((ulong)ILCodeVersioningStateLayout.Size, "ILCodeVersioningState")); | ||
|
|
||
| public MockILCodeVersionNode AddILCodeVersionNode(ulong versionId, uint rejitFlags, bool deoptimized = false, ulong ilAddress = 0) | ||
| public MockILCodeVersionNode AddILCodeVersionNode(ulong versionId, uint rejitFlags, bool deoptimized = false, uint source = 1 /* CodeVersionSource.ReJIT */, ulong encVersion = 0, ulong ilAddress = 0) |
| | ILCodeVersionNode | Source | a `CodeVersionSource` value indicating what produced this version (ReJIT, EnC, or unknown) | | ||
| | ILCodeVersionNode | InstrumentedILMap | Embedded `InstrumentedILOffsetMapping` describing the instrumented IL offset mapping | |
| { | ||
| if (!ilCodeVersionHandle.IsExplicit) | ||
| return false; | ||
| return // node Source is ReJIT |
| Module *pModule = GetModule(); | ||
|
|
||
| #ifdef FEATURE_CODE_VERSIONING | ||
| if (InEnCEnabledModule()) |
There was a problem hiding this comment.
Should both EnC and ReJIT be on the same plan? Either keep using dynamic IL to track last IL for both. Or drop dynamic IL for both and use dynamic IL for reflection emit only.
There was a problem hiding this comment.
ReJIT already uses the code version to store IL. It looks like we had just never captured ReJIT IL here. We probably should
There was a problem hiding this comment.
I wouldn't expect this method to get called for ReJIT methods and with the other changes in this PR I wouldn't expect ENC to call it either now. Unless there is something I am missing I'd suggest reverting this part of the change.
Both ReJIT and ENC should be getting their IL from the ILCodeVersion now:
ILCodeVersion::GetIL
VersionedPrepareCodeConfig::GetILHeader
GetAndVerifyMetadataILHeader
GetAndVerifyILHeader
JitCompileCodeLockedEventWrapper
JitCompileCode
PrepareCode
PublishVersionableCodeIfNecessary
DoPrestub
That VersionedPrepareCodeConfig is a derived type of PrepareCodeConfig that we use whenever compiling a non-default version of the code. The code in PublishVersionableCodeIfNecessary should detect the non-default version and use it.
There was a problem hiding this comment.
There are some other callers where I can't rule it out being called on an EnC method: for example, GetAndVerifyMetadataILHeader
There was a problem hiding this comment.
All calls to GetAndVerifyMetadataILHeader for a non-default version of a method are supposed to be using VersionedPrepareCodeConfig as the 'pConfig' argument which directs them down the code path I mentioned above and they don't get here. Do you see a path where VersionedPrepareCodeConfig won't be used for a non-default code version?
There was a problem hiding this comment.
There is also MethodDescGetILHeader and the various callers on the DAC side (task.cpp, stack.cpp).
There was a problem hiding this comment.
Ideally now that we have represented ENC code with explicit ILCodeVersions we should also only use version-aware handling of the IL. This ensures code paths get the IL that corresponds to particular version of the code they are introspecting as opposed to guessing that the default version or the latest version happens to be the right one. If that is a lot of work I don't mind if you include a comment saying the conversion is incomplete and assuming the caller wants the most recent ENC version is probably no worse than the behavior was in the past. However if we want to complete the conversion we should eliminate any code paths that call MethodDesc::GetILHeader() on non-default code versions.
For the MethodDescGetILHeader you mentioned the call path is:
-> Compiler::compCompile()
-> Compiler::eeGetVars()
-> ICorJitInfo::getVars / CEECodeGenInfo::getVars()
-> g_pDebugInterface->getVars(...)
-> Debugger::getVars(...)
-> g_pEEInterface->MethodDescGetILHeader(md)
-> MethodDesc::GetILHeader()
CEECodeGenInfo::getVars has access to the m_ILHeader for the exact version of the code being compiled so it could pass that along to Debugger::getVars() rather than having Debugger do a separate non-version-aware lookup.
noahfalk
left a comment
There was a problem hiding this comment.
This looked pretty good to me, a few suggestions inline.
|
|
||
| EX_TRY | ||
| { | ||
| ReJITID ilCodeVersionId = 0; |
There was a problem hiding this comment.
I'd add a comment in the code here explaining that this behavior retains compatibility with how we reported ENC updates in the past (ie not giving them unique IL code version IDs) but that we should be open to reconsidering the behavior in the future. I think the current behavior represents natural evolution and no scenario that cared about it rather than any intentional design goal that ENC versions should be reported differently than ReJIT versions.
| Module *pModule = GetModule(); | ||
|
|
||
| #ifdef FEATURE_CODE_VERSIONING | ||
| if (InEnCEnabledModule()) |
There was a problem hiding this comment.
I wouldn't expect this method to get called for ReJIT methods and with the other changes in this PR I wouldn't expect ENC to call it either now. Unless there is something I am missing I'd suggest reverting this part of the change.
Both ReJIT and ENC should be getting their IL from the ILCodeVersion now:
ILCodeVersion::GetIL
VersionedPrepareCodeConfig::GetILHeader
GetAndVerifyMetadataILHeader
GetAndVerifyILHeader
JitCompileCodeLockedEventWrapper
JitCompileCode
PrepareCode
PublishVersionableCodeIfNecessary
DoPrestub
That VersionedPrepareCodeConfig is a derived type of PrepareCodeConfig that we use whenever compiling a non-default version of the code. The code in PublishVersionableCodeIfNecessary should detect the non-default version and use it.
| // We have asked the profiler about this method via ICorProfilerFunctionControl, | ||
| // and have thus stored the IL and codegen flags the profiler specified. | ||
| // [cDAC] [ReJIT]: Contract depends on this value. | ||
| kStateActive = 0x00000002, |
There was a problem hiding this comment.
Please include a comment on kStateActive and on RejitFlags above noting that despite the 'Rejit' name we also use RejitFlags::kStateActive for ENC methods which always have their IL populated in advance.
| if (pMD->IsAsyncThunkMethod()) | ||
| { | ||
| pMD = pMD->GetAsyncVariantNoCreate(); | ||
| } |
There was a problem hiding this comment.
What if the method was actually an ordinary method with the thunk version being async? GetAsyncVariantNoCreate always gives the async version even if it is the thunk.
Would we want something like:
if (pMD->IsAsyncThunkMethod())
{
// The thunk delegates to the "other" variant which has the real IL body.
pMD = pMD->IsAsyncVariantMethod()
? pMD->GetOrdinaryVariantNoCreate() // Async thunk → real IL is on Ordinary
: pMD->GetAsyncVariantNoCreate(); // Ordinary thunk → real IL is on Async
}
Follow-up on #128338 (comment).
Sourcefield toILCodeVersionNodeto distinguish the sources.